package com.csx.data.orm.jpa.IdGenerator;

import com.csx.data.orm.jpa.IdGenerator.repositories.SerialNumberDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.*;



/**
 * @author: liyuan  liyuan1@sunline.cn
 * @date: 2020/3/9 17:21
 * @description: 生成id的测试类
 * @version: 4.0
 **/

@Component
public class SerialNumberService {

    private static final Logger logger = LoggerFactory.getLogger(SerialNumberService.class);

    private static final long serialVersionUID = 1L;

    //ID生成长度
    @Value("${serial.length:8}")
    public int length;

    //buffer号段消耗配置，0.8表示消耗80%
    @Value("${serial.capacity:0.8}")
    public double capacity;

    private ExecutorService service = new ThreadPoolExecutor(5, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new UpdateThreadFactory());
    //private volatile boolean initOK = false;
    private Map<String, SegmentBuffer> cache = new ConcurrentHashMap<String, SegmentBuffer>();

    public static class UpdateThreadFactory implements ThreadFactory {

        private static int threadInitNumber = 0;

        private static synchronized int nextThreadNum() {
            return threadInitNumber++;
        }

        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "Thread-Segment-Update-" + nextThreadNum());
        }
    }

    @Autowired
    private SerialNumberDao serialNumberDao;

    @PostConstruct
    public void init() {
        logger.info("Init serialNumber ID tag  from DB to cache");
        modifyCacheFromDb();
    }

    //读取数据库中的信息初始化缓存信息。
    private void modifyCacheFromDb() {
        logger.info("update cache from db");
        try {
            List<SerialNumberEntity> snList = serialNumberDao.findByEnable(true);
            //数据库信息表没有记录则抛出异常
            if (CollectionUtils.isEmpty(snList)) {
                logger.error("DB table gw_serial_number data is empty");
                throw new RuntimeException("DB table gw_serial_number data is empty.you should set id config in it.");
            }
            List<String> dbTags = new ArrayList<String>();
            for (SerialNumberEntity curSerial : snList) {
                dbTags.add(curSerial.getModuleTag());
                //如果复位时间为空则给其赋值
                if (curSerial.getResetTime() == null) {
                    Date resetTime = queryResetTime(new Date(), curSerial.getResetCycle(), curSerial.getResetUnit());
                    curSerial.setResetTime(resetTime);
                    serialNumberDao.save(curSerial);
                }
            }
            //缓存中的业务号段(项目启动时读取的信息)
            List<String> insertTags = new ArrayList<String>(dbTags);
            for (String tag : insertTags) {
                SegmentBuffer buffer = new SegmentBuffer();
                buffer.setKey(tag);
                //项目启动后每一个业务号段生成的buffer存入缓存中
                cache.put(tag, buffer);
                logger.info("Add tag {} from db to IdCache, SegmentBuffer {}", tag, buffer);
            }
        } catch (Exception e) {
            logger.error("update cache from db exception", e);
        }
    }

    //项目启动后新加的业务号段重新设入缓存中，针对新加的一个业务号段
    private void modifyCacheNewFromDb(SerialNumberEntity newSerialNumber) {
        //缓存中新加入新的业务号段
        SegmentBuffer buffer = new SegmentBuffer();
        buffer.setKey(newSerialNumber.getModuleTag());
        cache.put(newSerialNumber.getModuleTag(), buffer);
        //判读复位时间是否为空，为空设置。
        if (newSerialNumber.getResetTime() == null) {
            Date resetTime = queryResetTime(new Date(), newSerialNumber.getResetCycle(), newSerialNumber.getResetUnit());
            newSerialNumber.setResetTime(resetTime);
            serialNumberDao.save(newSerialNumber);
        }
        logger.info("Add new tag {} from db to IdCache, SegmentBuffer {}", newSerialNumber.getModuleTag(), buffer);
    }

    public String queryId(String moduleTag) {
        SegmentBuffer buffer = cache.get(moduleTag);
        if (buffer == null) {
            //根据业务号段到库里面查一下有没有这条信息
            SerialNumberEntity selectOne = serialNumberDao.findByEnableAndModuleTag(true, moduleTag);
            if (selectOne == null) {
                logger.error("enable is true and moduleTag is ({}) can not get a SerialNumber from DB table" +
                        " serial_number", moduleTag);
                throw new RuntimeException("enable is true and moduleTag is({" + moduleTag + "})  can not get a " +
                        "SerialNumber from DB table serial_number");
            } else {
                modifyCacheNewFromDb(selectOne);
                //从新在缓存中取出buffer赋值
                buffer = cache.get(moduleTag);
            }
        }
        synchronized (buffer) {
            if (!buffer.isInitOk()) {
                modifySegmentFromDb(moduleTag, buffer.getCurrent(), false);
                logger.info("Init buffer. Update leafkey {} {} from db", moduleTag, buffer.getCurrent());
                buffer.setInitOk(true);
            }
            // 改成同步复位 是否到达复位条件(时间周期)
            if (new Date().after(buffer.getCurrent().getResetTime())) {
                //复位最大分配ID 和 重置时间
                resetCurMaxByResetTime(buffer);
                //重新初始化值 当前分配号段最大值变为0
                buffer.getCurrent().setCurMax(0);
                modifySegmentFromDb(buffer.getKey(), buffer.getCurrent(), true);
                //下一个buffer重新初始化
                buffer.setNextReady(false);
                long value = buffer.getCurrent().getValue().getAndIncrement() + 1L;
                if (value <= buffer.getCurrent().getCurMax()) {
                    return generateId(value);
                }
            }
        }
        return queryIdFromSegmentBuffer(cache.get(moduleTag));
    }

    private String queryIdFromSegmentBuffer(SegmentBuffer buffer) {
        //尝试三次，抛异常
        int count = 0;
        while (true) {
            buffer.wLock().lock();
            try {
                Segment segment = buffer.getCurrent();
                // 如果下一个buffer未初始化成功  + 空闲的号段小于分配的最大可用号段（到达阈值）  +  buffer没有被其他线程占用  ==> 则进行初始化操作
                // ==> 使用到达阈值后，初始化下一个号段
                if (!buffer.isNextReady() && (segment.getIdle() <= (1 - capacity) * segment.getStep()) && buffer.getThreadRunning().compareAndSet(false, true)) {
                    initNextBuffer(buffer);  //异步初始化下一个号段
                }
                long value = segment.getValue().getAndIncrement() + 1L;
                if (value <= segment.getCurMax()) {
                    return generateId(value);
                } else {//第一个buffer取到呢最大值向第二个buffer中取值
                    try {
                        //等待1秒，提高下一个buffer初始化成功率
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (buffer.isNextReady()) {
                        buffer.switchPos();
                        buffer.setNextReady(false);
                        Segment twoSegment = buffer.getCurrent();
                        long twoValue = twoSegment.getValue().getAndIncrement() + 1L;
                        if (twoValue <= twoSegment.getCurMax()) {
                            return generateId(twoValue);
                        }
                    } else {
                        if (count >= 3) {
                            logger.error("Both two segments in {} are not ready!", buffer);
                            throw new RuntimeException("Both two segments in {" + buffer + "} are not ready!");
                        } else {
                            count++;
                        }
                    }
                }
            } finally {
                buffer.wLock().unlock();
            }
        }
    }


    /**
     * 异步初始化下一个号段
     *
     * @param buffer
     */
    private void initNextBuffer(SegmentBuffer buffer) {
        Segment segment = buffer.getCurrent();
        service.execute(new Runnable() {
            @Override
            public void run() {
                //判断是否达到自增ID复位最大值
                if (segment.getCurMax() >= segment.getResetMax()) {
                    resetCurMaxByResetMaxId(buffer.getKey());
                }
                //获取下一个buffer
                Segment next = buffer.getSegments()[buffer.nextPos()];
                boolean updateOk = false;
                try {
                    modifySegmentFromDb(buffer.getKey(), next, false);
                    updateOk = true;
                    logger.info("update segment {} from db {}", buffer.getKey(), next);
                } catch (Exception e) {
                    logger.warn("{} updateSegmentFromDb exception", buffer.getKey(), e);
                } finally {
                    if (updateOk) {
                        buffer.setNextReady(true);
                    }
                    buffer.getThreadRunning().set(false);
                }
            }
        });
    }

    /**
     * 功能说明 ：
     * 1、数据库分配号段-更新为下一个号段
     * 2、使用数据库的配置刷新当前缓存中的配置
     *
     * @param moduleTag
     * @param segment
     * @param resetFlag 判断是时间周期复位还是最大值ID复位
     */
    private void modifySegmentFromDb(String moduleTag, Segment segment, boolean resetFlag) {
        SerialNumberEntity serialNumber = serialNumberDao.findByEnableAndModuleTag(true, moduleTag);
        //对业务号段配置表中的信息进行非空校验
        if (serialNumber.getMaxId() == null
                || serialNumber.getCurConfMax() == null
                || StringUtils.isEmpty(serialNumber.getResetCycle())
                || serialNumber.getStep() == null
                || serialNumber.getResetTime() == null
                || serialNumber.getResetUnit() == null) {
            logger.error("please check the moduleTag is ({}) in table,maxId/curConfMax/resetCycle" +
                    "/step/resetTime/resetUnit,these can not be null!", moduleTag);
            throw new RuntimeException("please check the moduleTag is ({" + moduleTag + "}) in table,maxId/curConfMax/" +
                    "resetCycle/step/resetTime/resetUnit,these all can not be null!");
        }
        //号段长不能大于最大复位值
        if (serialNumber.getStep() > serialNumber.getMaxId()) {
            logger.error("please check the moduleTag is ({}) in table,step can not > maxId", moduleTag);
            throw new RuntimeException("please check the moduleTag is ({" + moduleTag + "}) in table,step can not > maxId");
        }
        //更新改业务号段的最大分配号段值为（号段长+当前分配好的最大号段值）
        if (resetFlag) {
            serialNumber.setCurConfMax(segment.getCurMax() + serialNumber.getStep());
        } else {
        	//如果当该业务号段的在已分配出去的最大值和复位最大值相等时重启项目，则当前分配出去最大需要复位
			if(serialNumber.getCurConfMax() >= serialNumber.getMaxId()) {
				serialNumber.setCurConfMax(serialNumber.getStep().longValue());
			}else {
				serialNumber.setCurConfMax(serialNumber.getCurConfMax() + serialNumber.getStep());
			}
        }
        serialNumber.setLastUpdateTime(new Date());
        serialNumberDao.save(serialNumber);
        //将获得的最新分配得到的最大号段取出来放入缓存中
        SerialNumberEntity newSerialNumber = serialNumberDao.findByEnableAndModuleTag(true, moduleTag);
        long value = newSerialNumber.getCurConfMax() - newSerialNumber.getStep();
        segment.getValue().set(value);
        //当前分配号段已经分配到的最大值
        segment.setCurMax(newSerialNumber.getCurConfMax());
        //最大复位数
        segment.setResetMax(newSerialNumber.getMaxId());
        segment.setStep(newSerialNumber.getStep());
        segment.setResetTime(newSerialNumber.getResetTime());
        segment.setResetCycle(newSerialNumber.getResetCycle());
        segment.setResetUnit(newSerialNumber.getResetUnit());
    }


    //将分配最大复位ID进行复位
    private void resetCurMaxByResetMaxId(String moduleTag) {
        SerialNumberEntity resetSerialNumber = serialNumberDao.findByModuleTag(moduleTag);
        resetSerialNumber.setCurConfMax(0L);
        resetSerialNumber.setLastUpdateTime(new Date());
        resetSerialNumber.setModuleTag(moduleTag);
        serialNumberDao.save(resetSerialNumber);
    }

    //根据时间周期规则复位
    private void resetCurMaxByResetTime(SegmentBuffer buffer) {
        Segment current = buffer.getCurrent();
        SerialNumberEntity resetSerialNumber = serialNumberDao.findByModuleTag(buffer.getKey());
        resetSerialNumber.setCurConfMax(0L);
        resetSerialNumber.setLastUpdateTime(new Date());
        //到达最大复位数是，复位时间不改变
        Date newResetTime = queryResetTime(current.getResetTime(), current.getResetCycle(), current.getResetUnit());
        resetSerialNumber.setResetTime(newResetTime);
        serialNumberDao.save(resetSerialNumber);
    }

    //计算复位时间
    private Date queryResetTime(Date baseTime, String resetCycle, Integer resetUnit) {
        GregorianCalendar gc = new GregorianCalendar();
        gc.setTime(baseTime);
        if ("D".equals(resetCycle)) {
            gc.add(Calendar.DATE, +resetUnit);
            return GwDateUtil.toDate(GwDateUtil.toString(gc.getTime()), GwDateUtil.YEAR_MONTH_DAY);
        } else if ("W".equals(resetCycle)) {
            gc.add(Calendar.WEEK_OF_YEAR, +resetUnit);
            return GwDateUtil.toDate(GwDateUtil.toString(gc.getTime()), GwDateUtil.YEAR_MONTH_DAY);
        } else if ("M".equals(resetCycle)) {
            gc.add(Calendar.MONTH, +resetUnit);
            return GwDateUtil.toDate(GwDateUtil.toString(gc.getTime()), GwDateUtil.YEAR_MONTH_DAY);
        } else if ("Y".equals(resetCycle)) {
            gc.add(Calendar.YEAR, +resetUnit);
            return GwDateUtil.toDate(GwDateUtil.toString(gc.getTime()), GwDateUtil.YEAR_MONTH_DAY);
        } else {
            return null;
        }
    }

    //ID生成规则
    private String generateId(Long id) {
        String curTime = GwDateUtil.toString(new Date(), GwDateUtil.YEARMONTHDAY);
        String formatStr = null;
        //长度不够8位则默认为8位
        if (length < 8) {
            formatStr = String.format("%0" + 8 + "d", id);
        } else if (length > 22) {//长度大于22位 默认长度22位
            formatStr = String.format("%0" + 22 + "d", id);
        } else {
            formatStr = String.format("%0" + length + "d", id);
        }
        return curTime + formatStr;
    }
}
